Segundo Parcial 2025
Primer ejercicio - Non-Blocking Concurrent Stack
Dada la siguiente implementación de un Stack, haz los cambios necesarios para que sea una estructura concurrente no bloqueante:
class Stack<E> {
class Node<E>(val item: E, var next: Node<E>? = null)
private var top: Node<E>? = null
fun push(item: E) {
val newHead = Node(item)
newHead.next = top
top = newHead
}
fun pop(): E? {
val oldHead = top
if (oldHead == null) return null
top.set(oldHead.next)
return oldHead.item
}
}
Nota: puede resolverse tanto en Kotlin como en Rust
Segundo ejercicio - Corutinas
Dada la siguiente corutina, predecir el output:
suspend fun compute(name: String, delayTime: Long): Int {
delay(delayTime)
println("Done with $name")
return name.length
}
fun main(): Unit = runBlocking {
println("Start")
launch {
val launchValue = compute("LaunchTask", 300L)
println("Launch result: $launchValue")
}
val deferred = async {
val result = compute("AsyncTask", 200L)
println("Async result: $result")
result
}
println("Middle")
val final = deferred.await()
println("Final result: $final")
}
Tercer ejercicio - Actores de una subasta
Implementar un sistema de subastas concurrente usando actores en Scala, con la librería Akka. El sistema involucra 3 tipos de actores:
- Auction / Subasta: recibe pujas y registra la más alta.
- Bidder / Pujador : envía pujas cada cierto tiempo
- Organizer / Organizador: empieza la subasta y recibe el resultado
La subasta empieza cuando el Auction recibe un mensaje del tipo StartAuction. Los Bidders envían las pujas (con un ID y una cantidad) por un tiempo específico.
Cuando la subasta termina, se le anuncia el ganador al Organizer.
- Define el protocolo (los mensajes que se intercambian)
- Implementar el comportamiento de cada actor
Código de ejemplo (main del programa)
object ActorSystem extends App {
val system = ActorSystem("AuctionSystem")
val organizer = system.actorOf(Props[Organizer], "organizer")
val auctionDuration = 15.seconds
val auction = system.actorOf(Props(new Auction(auctionDuration)), "auction")
auction ! StartAuction(organizer)
// Bidder A places bids up to 50 for 8 seconds
system.actorOf(Props(new Bidder(auction, "A", 50, 8.seconds)), "bidderA")
// Bidder B places bids up to 100 for 10 seconds
system.actorOf(Props(new Bidder(auction, "B", 100, 10.seconds)), "bidderB")
// Bidder C places bids up to 75 for 6 seconds
system.actorOf(Props(new Bidder(auction, "C", 75, 6.seconds)), "bidderC")
}
Operaciones útiles
// Send a "HELLO" message to itself after some duration
context.system.scheduler.scheduleOnce(duration, self, "HELLO")
// From the first moment the actor is created, after 50 milliseconds it sends itself a Tick message
context.system.scheduler.scheduleWithFixedDelay(Duration.Zero, 50.millis, self, Tick)
// Returns a random number from 0 to max
private val random = new Random()
random.nextInt(max)
Resolución
Primer ejercicio - Non-Blocking Concurrent Stack
Resolución en Kotlin
class NonBlockingStack<E> {
class Node<E>(val item: E, var next: Node<E>? = null)
private var top = AtomicReference<Node<E>?>()
fun push(item: E) {
val newHead = Node(item)
var oldHead: Node<E>?
do {
oldHead = top.get()
newHead.next = oldHead
} while (!top.compareAndSet(oldHead, newHead))
}
fun pop(): E? {
var oldHead: Node<E>? = top.get()
while (oldHead != null && !top.compareAndSet(oldHead, oldHead.next))
oldHead = top.get()
return oldHead?.item
}
}
Resolución en Rust
#![allow(unused)] fn main() { struct NonBlockingStack<T> { head: AtomicPtr<Node<T>>, size: AtomicUsize, } impl<T> NonBlockingStack<T> { pub fn new() -> Self { let dummy = Box::into_raw(Box::new(Node::dummy())); NonBlockingStack { head: AtomicPtr::new(dummy), size: AtomicUsize::new(0), } } fn push(&self, value: T) { let new_node = Box::into_raw(Box::new(Node::new(value))); loop { let head = self.head.load(Ordering::Acquire); unsafe { (*new_node).next.store(head, Ordering::Relaxed) }; if self .head .compare_exchange(head, new_node, Ordering::Release, Ordering::Acquire) .is_ok() { self.size.fetch_add(1, Ordering::Release); break; } } } fn pop(&self) -> Option<T> { loop { let cur_head = self.head.load(Ordering::Acquire); if cur_head.is_null() { return None; } let next_node = unsafe { (*cur_head).next.load(Ordering::Acquire) }; if self .head .compare_exchange(cur_head, next_node, Ordering::AcqRel, Ordering::Acquire) .is_ok() { self.size.fetch_sub(1, Ordering::Release); let old_head_node = unsafe { Box::from_raw(cur_head) }; return old_head_node.item; } } } } struct Node<T> { item: Option<T>, next: AtomicPtr<Node<T>>, } impl<T> Node<T> { fn dummy() -> Self { Node { item: None, next: AtomicPtr::new(null_mut()), } } fn new(item: T) -> Self { Node { item: Some(item), next: AtomicPtr::new(null_mut()), } } } }
Segundo ejercicio - Corutinas
El output va a ser el siguiente:
Start
Middle
Done with AsyncTask
Async result: 9
Final result: 9
Done with LaunchTask
Launch result: 10
¿Por qué?
El programa inicia printeando "Start". Luego, al llegar al launch (el cual es un scope de corutina que no devuelve ningún valor), entra a la función compute, ve que hay un delay y le cede el control a la corutina principal (la función main).
Acto seguido, vemos la inicialización de la corutina async, que también tiene un delay dado que llama a compute dentro suyo, por lo que también cede el control a la corutina principal.
Lo siguiente que se va a ejecutar es el print("Middle").
Luego, se queda esperando por el valor que devuelve el deferred, por lo que se ejecutará el print dentro de su llamado a compute (Done with AsyncTask), y luego se printeará el Async Result: 9.
Lo siguiente que pasará es que se printeará el valor de final result, dado que depende del valor de la tarea asíncrona.
Por último, se ejecutará el print del LaunchTask por volver a cederle el control al launch, seguido del Launch Result: 10.
Tercer ejercicio - Actores de una subasta
object AuctionProtocol {
case class Winner(id: String, amount: Int) // Se lo manda al Organizer
case object AnnounceWinner
case class StartAuction(organizer: ActorRef)
}
class Organizer extends Actor {
override def receive: Receive = {
case Winner(id: String, amount: Int) =>
println(s"The winner is $id with the incredible amount of $amount !")
}
}
class Auction(duration: FiniteDuration, organizer: ActorRef) extends Actor {
// Cuando se me termina el tiempo, anuncio el ganador
context.system.scheduler.scheduleOnce(duration, self, AuctionProtocol.AnnounceWinner)
private var currentMaxBid: (String, Int) = ("", 0)
override def receive: Receive = {
case AuctionProtocol.StartAuction(organizer: ActorRef) =>
println(s"The auction has started! It's being organized by ${organizer.toString}")
case BidderProtocol.Bid(id: String, amount: Int) =>
if (currentMaxBid._2 < amount) { // Si es una puja mayor, la actualizo
currentMaxBid = (id, amount)
}
case AuctionProtocol.AnnounceWinner =>
organizer ! Winner(currentMaxBid._1, currentMaxBid._2) // Le mando el ganador al organizador
context.stop(self) // Freno este actor
}
}
class Bidder(auction: ActorRef, id: String, maxBid: Int, duration: FiniteDuration) extends Actor {
// Programo para pujar cada cierto tiempo
context.system.scheduler.scheduleWithFixedDelay(Duration.Zero, 50.millis, self, BidderProtocol.SendBid)
private val random = Random()
private var currentBid: Int = 0
// Tengo que dejar de pujar después de un cierto tiempo
context.system.scheduler.scheduleOnce(duration, self, BidderProtocol.Stop)
override def receive: Receive = {
case BidderProtocol.Stop =>
context.stop(self) // Freno este actor cuando se me termina el tiempo (es decir, cuando dejo de pujar)
case BidderProtocol.SendBid =>
auction ! BidderProtocol.Bid(id, getCurrentMaxBid)
}
private def getCurrentMaxBid: Int = {
val newBid = random.nextInt(maxBid)
if (newBid > currentBid) {
currentBid = newBid
}
currentBid
}
}
object BidderProtocol {
case object SendBid
case class Bid(id: String, amount: Int)
case object Stop
}